home *** CD-ROM | disk | FTP | other *** search
- /*
- * Main controller for Stopwatch app.
- *
- * For legal stuff see the file COPYRIGHT
- */
- #import <stdio.h>
- #import <ansi/string.h> /* for import feature only (?) */
- #import <bsd/sys/param.h> /* for MAXPATHLEN */
- #import <appkit/NXCType.h>
- #import "Controller.h"
- #import "StopWatch.h"
- #import "InfoPanel.h"
- #import "ClientInfo.h"
- #import "ClientInspector.h"
- #import "SessionEditor.h"
- #import "AppIconView.h"
- #import "createPath.h"
- #import "Preferences.h"
-
- #define PRIORITY NX_MODALRESPTHRESHOLD
-
- #define ARCHIVE_FILE "client.data"
- #define TEMPLATE_DIR "Templates"
- #define MAXCLIENTLEN 80
-
- #define VERSION 2 /* the current file version that gets written */
- int FileVersion; /* the version of the file being read */
-
- void
- freeAndCopy( char **ptr, const char *str )
- {
- if ( *ptr )
- free( *ptr );
-
- *ptr = NXCopyStringBuffer(str);
- }
-
- const char *
- currentDate()
- {
- time_t now;
- struct tm *tm;
- static char buf[10];
-
- time(&now);
- tm = localtime(&now);
-
- sprintf( buf, "%02d/%02d/%02d", tm->tm_mon + 1, tm->tm_mday, tm->tm_year );
- return buf;
- }
-
- const char *
- currentTime()
- {
- time_t now;
- struct tm *tm;
- static char buf[10];
-
- time(&now);
- tm = localtime(&now);
-
- sprintf( buf, "%02d:%02d", tm->tm_hour, tm->tm_min );
- return buf;
- }
-
- /*
- * To avoid having to exec /bin/cp.
- */
- int
- copyFile( const char *src, const char *dst )
- {
- FILE *in, *out;
- int count;
- char buf[BUFSIZ];
-
- if ( ! (in = fopen( src, "r" ) ) ) {
- fprintf( stderr, "Can't open `%s' for reading.\n", src );
- return 0;
- }
-
- if ( ! (out = fopen( dst, "w" ) ) ) {
- fprintf( stderr, "Can't open `%s' for writing.\n", dst );
- fclose(in);
- return 0;
- }
-
- while ( (count = fread( buf, sizeof(char), sizeof(buf), in )) > 0 )
- fwrite( buf, sizeof(char), count, out );
-
- fclose(in);
- fclose(out);
- return 1;
- }
-
- @interface Controller(PRIVATE)
- - selectedClient;
- - (int)compare:obj1 :obj2; /* comparison method for SortList */
- - initInvoice;
- - (void)addSession:(const char *)startDate
- time:(const char *)startTime
- duration:(int)minutes
- description:(const char *)desc;
- - (void)checkStartButton;
- @end
-
-
- @implementation Controller
-
- DPSTimedEntryProc
- showElapsedTime(DPSTimedEntry teNum, double now, char *data)
- {
- [(id)data showElapsedTime];
- return (void *)NULL;
- }
-
- - (void) removeTimedEntry
- {
- if ( teNum ) {
- DPSRemoveTimedEntry(teNum);
- teNum = 0 ;
- }
- }
-
- - (void) addTimedEntry
- {
- [self removeTimedEntry]; /* in case there is one */
-
- /* Set it up so that the clock updates every minute */
- teNum = DPSAddTimedEntry( (double)60.0, (DPSTimedEntryProc)showElapsedTime,
- (void *)self, PRIORITY );
- }
-
- - free
- {
- [self removeTimedEntry];
- [stopwatch free];
- [infoPanel free];
- return [super free];
- }
-
- - awakeFromNib
- {
- [window setFrameAutosaveName:"Stopwatch"];
-
- /* Make the browser's font match the startButton (can't do this in IB) */
- [[[browser matrixInColumn:0] prototype] setFont:[startButton font]];
-
- return self;
- }
-
- - add:sender
- {
- [[ClientInspector sharedInstance] add:sender];
- return self;
- }
-
- - modify:sender
- {
- [[ClientInspector sharedInstance] modify:sender];
- return self;
- }
-
- - delete:sender
- {
- [[ClientInspector sharedInstance] delete:sender];
- return self;
- }
-
- - undelete:sender
- {
- [[ClientInspector sharedInstance] undelete:sender];
- return self;
- }
-
- - (void)enableAdd:(BOOL)flag
- {
- [addMenuItem setEnabled:flag];
- }
-
- - (void)enableModify:(BOOL)flag
- {
- [modifyMenuItem setEnabled:flag];
- }
-
- - (void)enableDelete:(BOOL)flag
- {
- [deleteButton setEnabled:flag];
- }
-
- - (void)enableUndelete:(BOOL)flag
- {
- [undeleteButton setEnabled:flag];
- }
-
- /*
- * Redisplay from the data in the clientList. Try to re-select the
- * same item afterwards.
- */
- - (void)decacheBrowser
- {
- /*
- * Find the possibly new position in the list of the selected client,
- * BEFORE redisplaying the browser from the list...
- */
- int row = [clientList indexOf:[self selectedClient]];
-
- [browser loadColumnZero];
- [[browser matrixInColumn:0] selectCellAt:row :0];
- [self checkStartButton];
- }
-
- - (NXTypedStream *)openArchive:(int)mode
- {
- NXTypedStream *stream ;
-
- if ( (stream = NXOpenTypedStreamForFile( filename, mode )) == NULL ) {
- NXRunAlertPanel( [NXApp appName], "Unable to open client data file: `%s'",
- "Create it when needed", NULL, NULL, filename );
- return nil;
- }
-
- return stream;
- }
-
- /*
- * Read in the client info from the typestream file
- */
- - (int)loadClientInfo
- {
- NXTypedStream *stream ;
-
- if ( (stream = [self openArchive:NX_READONLY]) == nil )
- return 0;
-
- NXReadType( stream, "i", &FileVersion );
- [clientList read:stream];
- [clientList sort];
- NXCloseTypedStream(stream) ;
-
- return 1;
- }
-
- - (int)saveClientInfoToStream:(NXTypedStream *)stream
- {
- int version = VERSION;
-
- NXWriteType( stream, "i", &version );
- [clientList write:stream];
- return 1;
- }
-
- - (int)saveClientInfo
- {
- NXTypedStream *stream ;
- char backup[FILENAME_MAX + 1];
-
- /*
- * If this is the first write, move the old filename to
- * filename~ to serve as a backup.
- */
- if ( didBackup == NO ) {
- sprintf( backup, "%s~", filename );
- rename( filename, backup );
- didBackup = YES;
- }
-
- if ( (stream = [self openArchive:NX_WRITEONLY]) == nil )
- return 0;
-
- [self saveClientInfoToStream:stream];
- NXCloseTypedStream(stream);
- return 1;
- }
-
- /*
- * Edit the selected invoicing template by messaging to the
- * workspace to open the corresponding file. Sender is the
- * Matrix containing the menu of template names.
- */
- - editTemplate:sender
- {
- id cell = [sender cellAt:[sender selectedRow] :0];
-
- [self initInvoice];
- [invoice editTemplate:[cell title]];
- return self;
- }
-
- - preferences:sender
- {
- [preferences display];
- return self;
- }
-
- - saveAs:sender
- {
- SavePanel *savePanel = [SavePanel new];
- NXTypedStream *stream;
- const char *path;
-
- if ( [savePanel runModalForDirectory:dirname file:""] == 0 )
- return nil;
-
- path = [savePanel filename];
-
- if ( (stream = NXOpenTypedStreamForFile( path, NX_WRITEONLY )) == NULL ) {
- NXRunAlertPanel( [NXApp appName], "Unable to open file for writing: `%s'",
- "What the...?", NULL, NULL, path );
- return nil;
- }
-
- [self saveClientInfoToStream:stream];
- NXCloseTypedStream(stream);
-
- return self;
- }
-
- - clientList
- {
- return clientList;
- }
-
- - appDidInit:sender
- {
- NXRect rect = {{0.0, 0.0}, {64.0, 64.0}};
-
- if ( createPath( dirname, DIRMODE ) != PathCreationOk ) {
- NXRunAlertPanel( [NXApp appName], "Cannot create path `%s'",
- "Damned UNIX!", NULL, NULL, dirname );
- [NXApp terminate:sender];
- }
-
- preferences = [Preferences new];
-
- [self loadClientInfo];
- [self decacheBrowser];
-
- if ( [preferences hideOnAutoLaunch] )
- [NXApp hide:self];
- else
- [window makeKeyAndOrderFront:self];
-
- /* make view that tracks elapsedTime be the appIcon window's contentView */
- appIconView = [[AppIconView alloc] initFrame:&rect
- sourceView:elapsedTimeField];
- [[[NXApp appIcon] setContentView:appIconView] free];
-
- [browser setDoubleAction:@selector(inspect:)];
- [browser setTarget:self];
-
- /* If there are no clients defined yet, disable the start buttons */
- [self checkStartButton];
-
- return self;
- }
-
- /*
- * If we logout, or there's a powerOff, make sure the time gets saved.
- */
- - app:sender powerOffIn:(int)ms andSave:(int)aFlag
- {
- return [self appWillTerminate:sender];
- }
-
- - appDidUnhide:sender
- {
- [window makeKeyAndOrderFront:self];
- return self;
- }
-
- - appWillTerminate:sender
- {
- if ( teNum )
- [self stopClock];
- return self;
- }
-
- - init
- {
- char path[FILENAME_MAX + 1];
-
- [super init];
-
- stopwatch = [[StopWatch alloc] init];
-
- clientList = [[SortList alloc] init];
- [clientList setAutoSort:YES];
- [clientList setDelegate:self];
-
- sprintf( path, "%s/Library/%s", NXHomeDirectory(), [NXApp appName] );
- dirname = NXCopyStringBuffer(path);
-
- sprintf( path, "%s/%s", dirname, ARCHIVE_FILE );
- filename = NXCopyStringBuffer(path);
-
- return self;
- }
-
- - (const char *) description
- {
- return [description stringValue];
- }
-
- /*
- * Called once per minute by the timed entry routine while the clock is running.
- */
- - showElapsedTime
- {
- [elapsedTimeField setStringValue:[stopwatch elapsedTime]];
- [appIconView display];
- return self;
- }
-
- /*
- * Respond to the user's selection of a client
- */
- - selectClient:sender
- {
- /* Assume that this means we should stop the previous client */
- if ( [stopwatch running] == YES )
- [startButton performClick:sender];
-
- activeClient = [self selectedClient];
- [description setStringValue:[activeClient lastDescription]];
- [description selectText:sender];
- return self;
- }
-
- /*
- * The start button highlights, but we need to force the title to "Stop".
- * Setting the Alternate Title didn't seem to do the right thing in IB.
- */
- - startClock
- {
- id font = [elapsedTimeField font];
-
- [elapsedTimeField setFont:[[FontManager new] convertWeight:YES of:font]];
- [self addTimedEntry];
- [startButton setTitle:"Stop"];
- [startMenuItem setTitle:"Stop"];
- [stopwatch startWatch];
- [self showElapsedTime];
- activeClient = [self selectedClient];
- return self;
- }
-
- /*
- * The mirror image of the above routine
- */
- - stopClock
- {
- id font = [elapsedTimeField font];
- [elapsedTimeField setFont:[[FontManager new] convertWeight:NO of:font]];
- [self removeTimedEntry];
- [startButton setTitle:"Start"];
- [startMenuItem setTitle:"Start"];
- [stopwatch stopWatch];
- [self showElapsedTime];
- [self addSession:[stopwatch startDateString]
- time:[stopwatch startTimeString]
- duration:[stopwatch elapsedMinutes]
- description:[self description]];
- activeClient = nil;
- return self;
- }
-
- /*
- * Called whenever the startButton is pressed.
- */
- - buttonHandler:sender
- {
- if ( [startButton state] == 1 )
- [self startClock];
- else
- [self stopClock];
-
- return self;
- }
-
- - showInfo:sender
- {
- [[InfoPanel new] showInfo];
- return self;
- }
-
- /*
- * Inspect the currently selected client
- */
- - inspect:sender
- {
- Matrix *matrix = [browser matrixInColumn:0];
- ClientInspector *inspector = [ClientInspector sharedInstance];
-
- [inspector selectClientAt:[matrix selectedRow]];
- [inspector display];
- return self;
- }
-
- - inspectSessions:sender
- {
- [[ClientInspector sharedInstance] showHours:sender];
- return self;
- }
-
- - inspectExpenses:sender
- {
- [[ClientInspector sharedInstance] showExpenses:sender];
- return self;
- }
-
- - inspectClients:sender
- {
- [[ClientInspector sharedInstance] showClient:sender];
- return self;
- }
-
- - generateDetail:sender
- {
- [self initInvoice];
- [invoice generate:clientList];
- return self;
- }
-
- /*
- * Find a client by short name
- */
- - (ClientInfo *)findClient:(const char *)name
- {
- int i, count = [clientList count];
-
- for ( i = 0; i < count; i++ ) {
- ClientInfo *info;
-
- info = [clientList objectAt:i];
- if ( strcmp( name, [info shortName] ) == 0 )
- return info ;
- }
- return nil;
- }
-
- /*
- * Compact consecutive sessions with identical descriptions into
- * a single session with the same total time.
- */
- - compactClients:sender
- {
- int i, count = [clientList count];
-
- for ( i = 0; i < count; i++ )
- [[clientList objectAt:i] compactSessions];
-
- [[ClientInspector sharedInstance] display];
-
- [self saveClientInfo];
- return self;
- }
-
- /*
- * This needs to be cleaned up...
- */
- - import:sender
- {
- FILE *fp;
- const char *pathname;
- char buf[512], *tok;
- char shortName[80], startDate[80], startTime[80], minutes[80], desc[256];
- id openPanel = [OpenPanel new];
- ClientInspector *inspector = [ClientInspector sharedInstance];
- char delimiter[2], endDelimiters[10];
-
- if ( [openPanel runModal] == 0 )
- return nil;
-
- pathname = [openPanel filename];
-
- if ( ! (fp = fopen( pathname, "r" ) ) ) {
- NXRunAlertPanel( [NXApp appName], "Unable to open import file: `%s'",
- "Eat me!", NULL, NULL, pathname );
- return self;
- }
-
- sprintf( delimiter, "%c", DELIMITER );
- sprintf( endDelimiters, "%c\n", DELIMITER );
-
- while ( fgets(buf, sizeof(buf), fp) ) {
- ClientInfo *info;
- Session *session;
-
- tok = strtok( buf, delimiter );
- strcpy( shortName, tok ) ;
-
- if ( ! (info = [self findClient:shortName]) ) {
- NXRunAlertPanel( [NXApp appName], "Ignoring unknown client: `%s'",
- "Who needs 'em?", NULL, NULL, shortName );
- continue;
- }
-
- tok = strtok( NULL, delimiter );
- strcpy( startDate, tok );
-
- tok = strtok( NULL, delimiter );
- strcpy( startTime, tok );
-
- tok = strtok( NULL, delimiter );
- strcpy( minutes, tok );
-
- tok = strtok( NULL, endDelimiters ); /* throw out newline too. */
- strcpy( desc, tok );
-
- session = [[Session alloc]
- init:startDate time:startTime
- duration:atoi(minutes) description:desc];
-
- [info addSession:session];
- [inspector updatedInfo:info];
- }
-
- fclose(fp);
- [self saveClientInfo];
- return self;
- }
-
- - export:sender
- {
- FILE *fp;
- const char *pathname;
- int i, count = [clientList count];
- id savePanel = [SavePanel new];
-
- if ( [savePanel runModal] == 0 )
- return nil;
-
- pathname = [savePanel filename];
-
- if ( ! (fp = fopen( pathname, "w" ) ) ) {
- NXRunAlertPanel( [NXApp appName], "Unable to open export file: `%s'",
- "I'll be darned!", NULL, NULL, pathname );
- return self;
- }
-
- for ( i = 0; i < count; i++ )
- [[clientList objectAt:i] exportToFile:fp];
-
- fclose(fp);
- return self;
- }
-
- /*
- * Clear out all session information from all clients.
- */
- - closeMonth:sender
- {
- ClientInspector *inspector = [ClientInspector sharedInstance];
-
- /* Give the user a chance to change their mind... */
- if ( NXRunAlertPanel( [NXApp appName], "Delete all session and expense data?",
- "Delete all data", "Hell no!", NULL ) == NX_ALERTDEFAULT ) {
- [clientList makeObjectsPerform:@selector(deleteSessionsAndExpenses)];
- [inspector closeMonth];
- [self saveClientInfo]; /* write the newly empty file */
- [inspector display];
- }
-
- return self;
- }
-
- @implementation Controller(PRIVATE)
-
- - initInvoice
- {
- char path[FILENAME_MAX + 1];
-
- if ( invoice == nil ) {
- sprintf( path, "%s/%s", dirname, TEMPLATE_DIR );
- invoice = [[Invoice alloc] initTemplateDir:path];
- }
- return invoice;
- }
-
- - (int)selectedRow
- {
- return [[browser matrixInColumn:0] selectedRow];
- }
-
- - selectedClient
- {
- return [clientList objectAt:[self selectedRow]];
- }
-
- /*
- * Make sure the start buttons are disabled if there are
- * no clients defined, and enabled if there are.
- */
- - (void)checkStartButton
- {
- BOOL flag = ( [clientList count] ? YES : NO );
-
- [startMenuItem setEnabled:flag];
- [startButton setEnabled:flag];
-
- /* the same goes for these */
- [sessionMenuItem setEnabled:flag];
- [expenseMenuItem setEnabled:flag];
-
- /*
- * The only reasonable thing to do if there are no
- * clients is to create some!
- */
- if ( flag == NO )
- [self inspectClients:nil];
- }
-
- /*
- * Create a new session object and add it to the proper client's
- * ClientInfo list. Tell the browser what happened so it can update.
- */
- - (void)addSession:(const char *)startDate
- time:(const char *)startTime
- duration:(int)minutes
- description:(const char *)desc
- {
- Session *session = [[Session alloc]
- init:startDate time:startTime
- duration:minutes description:desc];
- [activeClient addSession:session];
- [[ClientInspector sharedInstance] updatedInfo:activeClient];
- [self saveClientInfo];
- }
-
- /*
- * Compare two ClientInfo objects. Sort alpha by long name.
- */
- - (int)compare:obj1 :obj2
- {
- return strcmp( [obj1 clientName], [obj2 clientName] );
- }
-
- /*
- * Delegated method of NXBrowser. This should be consolidated into a single
- * object. Right now this method appears (almost) identically in the Controller
- * and the ClientMgr... (Here we use the shortName instead of the full one.)
- */
- - (int) browser:sender fillMatrix:matrix inColumn:(int)column
- {
- int i, count = [clientList count];
-
- for ( i = 0; i < count; i++ ) {
- const char *name;
- id cell;
-
- [matrix addRow];
- name = [[clientList objectAt:i] shortName];
- cell = [matrix cellAt:i :0]; /* 1 dimen. matrix: always use col 0! */
- [cell setStringValue:name];
- [cell setLoaded:YES];
- [cell setLeaf:YES];
- }
-
- return count ;
- }
-
- @end
-